<?php

namespace framework\core;

use Psr\Log\LoggerInterface;

/**
 * 日志类
 */
class Log implements LoggerInterface {

    private $_messages = []; /* 日志数据 */

    /**
     * 配置参数
     * @var array
     */
    protected $config = [
        'path' => '',
        'time_format' => 'Y-m-d H:i:s',
        'json' => true,
        'json_options' => JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES,
        'format' => '[%s][%s] %s',
        'retention_time' => 3 * 24 * 3600,
    ];

    public function __construct() {

        $config = Config::getInstance()->get('log_config');
        if (is_array($config)) {
            $this->config = array_merge($this->config, $config);
        }

        if (empty($this->config['format'])) {
            $this->config['format'] = '[%s][%s] %s';
        }

        if (empty($this->config['path'])) {
            $this->config['path'] = ROOT_PATH . "cache/logs/";
        }

        /* 自动创建日志目录 */
        $log_dir = dirname($this->config['path']);
        if (is_dir($log_dir) == false) {
            mkdir($log_dir, 0777);
        }

        /* 默认保留1天 */
        if ($this->config['retention_time'] <= 0) {
            $this->config['retention_time'] = 24 * 3600;
        }
    }

    public static function getInstance() {
        static $obj;
        if (!$obj) {
            $obj = new self();
        }
        return $obj;
    }

    /**
     * 严重错误
     * @param type $message
     * @param array $context
     * @return $this
     */
    public function emergency(string|\Stringable $message, array $context = []): void {
        $this->log('emergency', $message, $context);
    }

    /**
     * **必须**立刻采取行动
     * @param type $message
     * @param array $context
     * @return $this
     */
    public function alert(string|\Stringable $message, array $context = []): void {
        $this->log('alert', $message, $context);
    }

    /**
     * 紧急情况
     * @param type $message
     * @param array $context
     * @return $this
     */
    public function critical(string|\Stringable $message, array $context = []): void {
        $this->log('critical', $message, $context);
    }

    /**
     * 运行时出现的错误，不需要立刻采取行动，但必须记录下来以备检测。
     * @param type $message
     */
    public function error(string|\Stringable $message, array $context = []): void {
        $this->log('error', $message, $context);
    }

    /**
     * 出现非错误性的异常
     * @param type $message
     * @param array $context
     * @return $this
     */
    public function warning(string|\Stringable $message, array $context = []): void {
        $this->log('warning', $message, $context);
    }

    /**
     * 一般性重要的事件
     * @param type $message
     * @param array $context
     * @return $this
     */
    public function notice(string|\Stringable $message, array $context = []): void {
        $this->log('notice', $message, $context);
    }

    /**
     *  信息: 程序输出信息
     * @param type $message
     * @param array $context
     * @return $this
     */
    public function info(string|\Stringable $message, array $context = []): void {
        $this->log('info', $message, $context);
    }

    /**
     * 调试: 调试信息
     * @param type $message
     * @param array $context
     * @return $this
     */
    public function debug(string|\Stringable $message, array $context = []): void {
        $this->log('debug', $message, $context);
    }

    /**
     * 记录 sql
     * @param type $message
     */
    public function sql(string|\Stringable $message, array $context = []): void {
        $this->log('sql', $message, $context);
    }

    /**
     * 记录日志
     *
     * @param mixed $level 日志级别
     * @param string|\Stringable $message 日志消息
     * @param array $context 上下文信息
     * @throws InvalidArgumentException
     */
    public function log($level, string|\Stringable $message, array $context = []): void {

        /* 处理消息中的占位符 */
        $message = $this->interpolate($message, $context);

        /* 生成日志条目 */
        $logEntry = $this->formatLogEntry($message, $context);

        $this->_messages[$level][] = $logEntry;

        if (php_sapi_name() == "cli") {
            /* 因为命令行不退出，所以直接输出 */
            $this->flush();
        }
    }

    /**
     * 格式化日志条目
     */
    private function formatLogEntry(string $message, array $context): string {

        $logEntry = "{$message}";

        // 如果有上下文信息，添加到日志中
        if (!empty($context)) {
            $contextStr = var_export($context, true);
            $logEntry .= "\n [CONTEXT] \n {$contextStr}";
        }

        return $logEntry;
    }

    /**
     * 处理消息中的占位符
     * 将 {key} 替换为 context 中对应的值
     */
    private function interpolate($message, array $context = []) {
        $replace = [];

        if (!is_string($message) && !($message instanceof \Stringable)) {
            throw new InvalidArgumentException('消息必须是字符串或可字符串化对象');
        }

        $message = (string) $message;

        // 如果没有占位符，直接返回
        if (strpos($message, '{') === false) {
            return $message;
        }

        $replace = [];
        foreach ($context as $key => $val) {
            // 检查值是否可安全转换为字符串
            if (
                    is_null($val) ||
                    is_scalar($val) ||
                    (is_object($val) && method_exists($val, '__toString'))
            ) {
                $replace['{' . $key . '}'] = $val;
            } elseif (is_object($val)) {
                $replace['{' . $key . '}'] = '[object ' . get_class($val) . ']';
            } elseif (is_array($val)) {
                $replace['{' . $key . '}'] = '[array]';
            } else {
                $replace['{' . $key . '}'] = '[' . gettype($val) . ']';
            }
        }

        return strtr($message, $replace);
    }

    public function write(array $messages = []) {

        $ctime = date($this->config['time_format']);

        $info = [];
        foreach ($messages as $level => $val) {
            foreach ($val as $msg) {
                if (!is_string($msg)) {
                    $msg = var_export($msg, true);
                }
                if ($this->config['json']) {
                    $info[$level][] = json_encode(['ctime' => $ctime, 'level' => $level, 'message' => $msg], $this->config['json_options']);
                } else {
                    $info[$level][] = sprintf($this->config['format'], $ctime, $level, $msg);
                }
            }
        }
        unset($messages);

        foreach ($info as $level => $msg) {
            $contents = is_array($msg) ? implode(PHP_EOL, $msg) . PHP_EOL : $msg . PHP_EOL;

            $destination = $this->config['path'] . "{$level}_" . date('Y_m_d_H') . '.log';

            file_put_contents($destination, $contents, FILE_APPEND);

            @chmod($destination, 0777);
        }
    }

    public function flush() {
        if (empty($this->_messages)) {
            return;
        }
        $this->write($this->_messages);
        $this->_messages = [];
    }

    /**
     * 清理3天前的日志文件
     * @return bool
     */
    public function clear_log_file(): bool {

        $dir_iterator = new \RecursiveDirectoryIterator($this->config['path']);
        $iterator = new \RecursiveIteratorIterator($dir_iterator);

        $mtime = time() - $this->config['retention_time'];

        foreach ($iterator as $file) {
            if ($file->isDir()) {
                continue;
            }

            $destination = $file->getPath() . '/' . $file->getFilename();

            if ($mtime > $file->getMTime()) {
                unlink($destination);
            }
        }

        return true;
    }

    public function __destruct() {

        $this->flush();

        $this->clear_log_file();
    }
}
